package common.parser;

import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Hashtable;


/**
 * This class adds the facility to read, store and retrieve the information
 * contained in the methods of a class. It stores all those data of one class.
 * Access flags are not considered. The method bodies are parsed and their code
 * is made available via public access methods. This class should be subclassed
 * to a version with an actual proof encoded. To do enable parsing this,
 * overwrite read_method_Attribute_Info as in the example
 * JavaClassWithIntervals, or the respective other read method.
 * 
 * @author Jan, Steffen Lortz
 */
public class JavaClass extends AbstractJavaClass {
	

	/** Contains the code of each method */
	// Stores byte[]s, indexed by full method name Strings. (name and
	// parameter_list)
	protected Hashtable methodCodes = new Hashtable();
	
	// For storage, generic data structures are used since generics are not
	// available in J2ME
	// The data structures can be accessed via the public methods of this class.
	// Structures and access methods for the constants pool are found in the
	// superclass.
	
	/** Contains the indices of each instruction for each method */
	// Stores int[]s, indexed by full method name Strings
	protected Hashtable methodLines = new Hashtable();
	
	/** Contains each method's number of instructions */
	// Stores Integers, indexed by full method name Strings.
	protected Hashtable methodLengths = new Hashtable();
	

	public JavaClass(InputStream is) {
		super(is);
	}
	
	// ------------------------------ Public Part
	// --------------------------------------------
	
	/**
	 * Returns all parameter bytes of one instruction
	 * 
	 * @param qualifying_method_name The qualifying name of the method
	 * @param instrNo The line number of the instruction
	 * @return The parameter bytes
	 */
	public byte[] getArgs(String qualifying_method_name, int instrNo) {
		
		int instrPos = ((int[]) this.methodLines.get(qualifying_method_name))[instrNo];
		int length = this.instruction_Param_Length(qualifying_method_name, instrPos);
		byte[] args = new byte[length];
		for (int i = 0; i < length; i++) {
			args[i] = ((byte[]) this.methodCodes.get(qualifying_method_name))[instrPos + 1 + i];
		}
		return args;
	}
	
	/**
	 * Returns the opcode of the instruction indicated by methodName and InstrNo
	 * 
	 * @param qualifying_method_name The fully qualifying name of the method
	 * @param instrNo The line number of the instruction
	 */
	public int getInstructionOpcode(String qualifying_method_name, int instrNo) {
		// access byte[] storing the code
		byte instruction = ((byte[]) this.methodCodes.get(qualifying_method_name))
		// access instruction
		[((int[]) this.methodLines.get(qualifying_method_name))[instrNo]];
		// Map signed byte to values 00 .. 255
		return (instruction >= 0) ? instruction : 256 + instruction;
	}
	
	/**
	 * Returns the branch target of the specified instruction, or 0 if the
	 * instruction is not actually a branch. Warning: Currently implements only
	 * for GOTO IFEQ IFNE IFLT IFGE IFGT IFLE IFNULL IF_ICMPEQ IF_ICMPNE
	 * IF_ICMPLT IF_ICMPGE IF_ICMPGT IF_ICMPLE
	 * 
	 * @param qualifying_method_name The signature of the method, including its
	 *            return values
	 * @param instrNo The line number of the instruction
	 * @return The instruction's relative branch target
	 */
	public int getJumpTarget(String qualifying_method_name, int instrNo)
			throws IllegalArgumentException {
		// calculation from JVM-specification
		byte[] args = this.getArgs(qualifying_method_name, instrNo);
		int branchoffset = (args[0] << 8) | (args[1]); // ... & ((short)
		// 0x00ff));
		int[] localLines = (int[]) this.methodLines.get(qualifying_method_name);
		int targetpos = localLines[instrNo] + branchoffset;
		
		// search lines for instruction number with targetpos as position
		for (int i = 0; i < localLines.length; i++) {
			if (localLines[i] == targetpos) {
				return i;
			}
		}
		
		// if no appropriate position could be found, throw exception
		throw new IllegalArgumentException("Invalid offset. Target position: " + targetpos
				+ " at instruction " + instrNo + " (position in codelines:  " + localLines[instrNo]
				+ ")");
	}
	
	/**
	 * Returns the name of the field which is the target argument at the
	 * specified code point.
	 * 
	 * @param qualifying_method_name the method
	 * @param instrNo the line of the method
	 * @return the index of the addressed field
	 */
	public String getTargetField(String qualifying_method_name, int instrNo) {
		byte[] args = this.getArgs(qualifying_method_name, instrNo);
		int index = (args[0] << 8) | (args[1]);
		int tmp = this.constPoolEntries[index];
		int class_index = tmp >> 16;
		int name_and_type_index = (tmp & 0x0000FFFF);
		tmp = this.constPoolEntries[class_index];
		tmp = this.UtfsPos[tmp];
		String class_name = new String((byte[]) this.Utfs.elementAt(tmp));
		tmp = this.constPoolEntries[name_and_type_index];
		int name_index = tmp >> 16;
		tmp = this.UtfsPos[name_index];
		String field_name = new String((byte[]) this.Utfs.elementAt(tmp));
		return class_name + "." + field_name;
	}
	
	/**
	 * Returns the name of the target method of the given program point.
	 * 
	 * @param qualifying_method_name the method
	 * @param instrNo the line of the method
	 * @return the qualifying name of the target method
	 */
	public String getTargetMethod(String qualifying_method_name, int instrNo) {
		byte[] args = this.getArgs(qualifying_method_name, instrNo);
		int index = (args[0] << 8) | (args[1]);
		int tmp = this.constPoolEntries[index];
		int class_index = tmp >> 16;
		int name_and_type_index = (tmp & 0x0000FFFF);
		tmp = this.constPoolEntries[class_index];
		tmp = this.UtfsPos[tmp];
		String class_name = new String((byte[]) this.Utfs.elementAt(tmp));
		tmp = this.constPoolEntries[name_and_type_index];
		int name_index = tmp >> 16;
		int descriptor_index = (tmp & 0x0000FFFF);
		tmp = this.UtfsPos[name_index];
		String method_name = new String((byte[]) this.Utfs.elementAt(tmp));
		tmp = this.UtfsPos[descriptor_index];
		String descriptor_name = new String((byte[]) this.Utfs.elementAt(tmp));
		return class_name + "." + method_name + descriptor_name;
	}
	
	/**
	 * Returns the name of the target class of the given program point.
	 * 
	 * @param qualifying_method_name the method
	 * @param instrNo the line of the method
	 * @return the qualifying name of the target class
	 */
	public String getTargetClass(String qualifying_method_name, int instrNo) {
		byte[] args = this.getArgs(qualifying_method_name, instrNo);
		int index = (args[0] << 8) | (args[1]);
		int class_index = this.constPoolEntries[index];
		int tmp = this.constPoolEntries[class_index];
		tmp = this.UtfsPos[tmp];
		String class_name = new String((byte[]) this.Utfs.elementAt(tmp));
		return class_name;
	}
	
	/** Returns the number of lines of a method */
	public int getMethodLength(String qualifying_method_name) {
		return ((Integer) this.methodLengths.get(qualifying_method_name)).intValue();
	}
	
	/** Returns the name of all methods of this class */
	public String[] getMethods() {
		String[] methods = new String[this.methodCodes.size()];
		Enumeration e = this.methodCodes.keys();
		int i = 0;
		while (e.hasMoreElements()) {
			methods[i++] = ((String) e.nextElement());
		}
		return methods;
	}
	
	// -------------------------------- Read the method code
	// ----------------------------------
	
	protected String getQualifyingMethodName(int name, int descriptor) {
		String methodName = new String(this.get_Utf8_info(name));
		String methodDescriptor = new String(this.get_Utf8_info(descriptor));
		return this.getClassName() + "." + methodName + methodDescriptor;
	}
	
	protected void readCodeblock(int methodName, int descriptor) throws IOException,
			SyntaxException {
		
		this.bcode.readInt(); // attribute_length, not relevant for code
		// attribute
		this.bcode.readShort(); // max_stack;
		this.bcode.readShort(); // max_locals;
		
		// read code_length and make space for the code
		byte[] code_bytes = new byte[this.bcode.readInt()];
		this.bcode.read(code_bytes);
		
		String name = this.getQualifyingMethodName(methodName, descriptor);
		this.methodCodes.put(name, code_bytes);
		this.organize_methods_bytecode(name);
		
		int exception_table_length = this.bcode.readShort();
		int i;
		for (i = 0; i < exception_table_length; i++) {
			this.bcode.readShort(); // start_pc
			this.bcode.readShort(); // end_pc
			this.bcode.readShort(); // handler_pc
			this.bcode.readShort(); // catch_type
		}
		int attributes_count = this.bcode.readShort();
		for (i = 0; i < attributes_count; i++) {
			this.skip_attribute_Info();
		}
	}
	
	/*
	 * For one method, identifies the position of each instruction within the
	 * blocks of code and writes them into the hash table MethodLines (see
	 * there)
	 */
	protected void organize_methods_bytecode(String qualifying_method_name) throws SyntaxException {
		
		byte[] code = (byte[]) this.methodCodes.get(qualifying_method_name);
		int current_line_no = 0;
		int position_in_codeblock = 0;
		
		// Array to store where each instruction begins, the length set so that
		// it will fit even
		// if all commands are unary
		int[] lines = new int[code.length];
		
		// Each iteration deals with one instruction
		while (position_in_codeblock < code.length) {
			lines[current_line_no++] = position_in_codeblock;
			
			byte instruction = code[position_in_codeblock];
			
			if (0xcb < instruction) {
				throw new SyntaxException("Encountered an unused opcode");
			}
			
			// select the next instruction
			position_in_codeblock += 1 + this.instruction_Param_Length(code, position_in_codeblock);
		}
		this.methodLines.put(qualifying_method_name, lines);
		this.methodLengths.put(qualifying_method_name, new Integer(current_line_no));
	}
	
	/*
	 * Computes the actual length of an instruction in the bytecode of a method.
	 * This value may depend on the position of the instruction in the code for
	 * operations that are encoded using padding, and on values specified as
	 * arguments of the instruction itself. The returned value is undefined if
	 * the
	 * @param methodCode The Code of a method
	 * @param pos The position of a byte representing an opcode within the
	 * codeblock
	 * @return The length of the instruction
	 */
	protected int instruction_Param_Length(byte[] methodCode, int pos) {
		
		byte instruction = methodCode[pos];
		
		// Handle four special cases, otherwise just lookup the value
		if (instruction == JvmOpcodes.TABLESWITCH) {
			int pad = (3 - (pos % 4)); // Padding bytes
			int low = (methodCode[pos + pad + 5] << 24) | (methodCode[pos + pad + 6] << 16)
					| (methodCode[pos + pad + 7] << 8) | (methodCode[pos + pad + 8]);
			int high = (methodCode[pos + pad + 9] << 24) | (methodCode[pos + pad + 10] << 16)
					| (methodCode[pos + pad + 11] << 8) | (methodCode[pos + pad + 12]);
			return pad + 12 + 4 * (high - low + 1); // 12 bytes plus (high-low +
			// 1)
		}
		if (instruction == JvmOpcodes.LOOKUPSWITCH) {
			int pad = (3 - (pos % 4)); // Padding bytes
			int npairs = (methodCode[pos + pad + 5] << 24) | (methodCode[pos + pad + 6] << 16)
					| (methodCode[pos + pad + 7] << 8) | (methodCode[pos + pad + 8]);
			return pad + 8 + 4 * 2 * npairs;
		}
		if (instruction == JvmOpcodes.WIDE) {
			return ((JvmOpcodes.IINC == methodCode[pos + 1]) ? 5 : 3);
		}
		
		// Remove the sign from the byte
		int lookup = (instruction >= 0) ? instruction : 256 + instruction;
		return OperandLengths.operands[lookup];
	}
	
	/* delegates to the (byte[],int)-version */
	protected int instruction_Param_Length(String methodName, int pos) {
		return this.instruction_Param_Length((byte[]) this.methodCodes.get(methodName), pos);
	}
	
	protected final void read_method_Attribute_Info(int methodName, int descriptor)
			throws IOException, SyntaxException {
		String attributeName = new String(this.get_Utf8_info(this.bcode.readShort()));
		// When a method is non-abstract, the Code attribute contains the
		// bytecode
		boolean isRead = this.readMethodAttributeInfo(attributeName, methodName, descriptor);
		if (!isRead && attributeName.equals("Code")) {
			this.readCodeblock(methodName, descriptor);
		} else if (!isRead) {
			this.skipAttributeInfo();
		}
	}
	
	/**
	 * Skips the rest of an attribute info.
	 * 
	 * @throws IOException on end of file
	 */
	protected final void skipAttributeInfo() throws IOException {
		int attribute_length = this.bcode.readInt();
		for (int l = 0; l < attribute_length; l++) {
			this.bcode.readByte(); // info[attribute_length]
		}
	}
	
	/**
	 * Reads a method attribute from the class's input stream. Must return true
	 * if the attribute was read. If no instance is interested, the attribute is
	 * skipped. The "Code" attribute is also provided in case one wants to parse
	 * methods in a different way than JavaClass does.
	 * 
	 * @param attributeName the name of the attribute
	 * @param methodName the method's name index
	 * @param descriptor the method's descriptor index
	 * @return true if the attribute was read, false otherwise
	 * @throws IOException if the stream cannot be read
	 */
	protected boolean readMethodAttributeInfo(String attributeName, int methodName, int descriptor)
			throws IOException {
		return false;
	}
	
}
